package lukeer.fencing;

import csproj.fencing.Move;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Holds current board configuration and contains algorithm to calculate result of the board. Players create instances
 * of this class for tree generation and to hold the currentGameState.
 *
 * User: Luke Richardson, Wafa M'Barek
 * Date: 29/07/12
 * Time: 16:38
 */
public class Board implements csproj.fencing.Piece {
    public enum Direction {
        UP, DOWN, LEFT, RIGHT
    }

    private final int boardSize;
    private final BoardPiece[][] boardPieces;
    private final boolean[][] visited;
    private final ArrayList<Sector> sectors;
    private int totalBlack;
    private int totalWhite;
    private int result = INVALID;

    /**
     * Create empty board of a given input size
     *
     * @param size Board arrays are instantiated to size x size
     */
    public Board(int size) throws FencingException {
        boardSize = size;
        if (size < 1) throw new FencingException("Invalid board size: " + size);
        boardPieces = new BoardPiece[size][size];
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                boardPieces[i][j] = new BoardPiece(i, j);
                if (i == 0) boardPieces[i][j].setHFence();
                if (j == 0) boardPieces[i][j].setVFence();
            }
        }
        visited = new boolean[size][size];
        sectors = new ArrayList<Sector>();
        this.calculateResult();
    }

    /**
     * Copy constructor
     *
     * @param b deep copy of board
     */
    public Board(Board b) {
        boardSize = b.size();
        boardPieces = new BoardPiece[boardSize][boardSize];
        for (int i = 0; i < boardSize; i++) {
            for (int j = 0; j < boardSize; j++) {
                boardPieces[i][j] = new BoardPiece(b.getPiece(i, j));
            }
        }
        visited = new boolean[boardSize][boardSize];
        sectors = new ArrayList<Sector>();
        this.calculateResult();
    }

    /**
     * Returns the piece object for a given position on the board
     *
     * @param x x co-ordinate
     * @param y y co-ordinate
     * @return BoardPiece object at board[x][y]
     */
    public BoardPiece getPiece(int x, int y) {
        return boardPieces[x][y];
    }

    /**
     * Returns true if a piece is within an eliminated sector
     *
     * @param x x position
     * @param y y position
     * @return true if piece is in eliminated sector
     */
    private boolean inEliminatedSector(int x, int y) {
        BoardPiece p = getPiece(x, y);
        for (Sector s : sectors) {
            if (s.contains(p)) {
                if (s.isEliminated()) return true;
            }
        }
        return false;
    }

    /**
     * Return sector list
     *
     * @return ArrayList of sectors
     */
    public ArrayList<Sector> getSectors() {
        return sectors;
    }

    /**
     * Place a counter in a given position
     *
     * @param x    x position
     * @param y    y position
     * @param type BLACK or WHITE
     * @throws FencingException throws FencingExcpetion if a piece is already placed
     */
    void setPiece(int x, int y, int type) throws FencingException {
        if (type != EMPTY & !inEliminatedSector(x, y)) {
            if (boardPieces[x][y].getType() != EMPTY) {
                throw new FencingException("BoardPiece already placed");

            }
            getPiece(x, y).setType(type);
        }
    }

    /**
     * Place a vertical fence in the given position
     *
     * @param x x position
     * @param y y position
     * @throws FencingException throws FencingException if a fence already exists
     */
    void setVFence(int x, int y) throws FencingException {
        if ((y == 0) || x > boardSize - 1 || y > boardSize - 1)
            throw new FencingException("No fence in the border of the board" + x + "," + y);
        if (inEliminatedSector(x, y)) throw new FencingException("BoardPiece in eliminated sector");
        BoardPiece p = getPiece(x, y);
        if (p.getVFence())
            throw new FencingException("Vertical fence already exists");
        p.setVFence();
    }

    /**
     * Place a horizontal fence in the given position
     *
     * @param x x position
     * @param y y position
     * @throws FencingException throws FencingException if a fence already exists
     */

    void setHFence(int x, int y) throws FencingException {
        if ((x == 0) || x > boardSize - 1 || y > boardSize - 1)
            throw new FencingException("No fence in the border of the board" + x + "," + y);
        if (inEliminatedSector(x, y)) throw new FencingException("BoardPiece in eliminated sector");
        BoardPiece p = getPiece(x, y);
        if (p.getHFence()) throw new FencingException("Horizontal fence already exists");
        p.setHFence();
    }


    /**
     * Returns the end result of the game which is generated by calculateResult()
     *
     * @return a Result enum value
     */
    public int getResult() {
        return result;
    }

    /**
     * Iterates through all sectors and checks if the sector fulfils the requirement for a player to win the game.
     * This is done by invoking the isWinnable() method on each sector.
     * If there exists more than one sector that isWinnable(), then the game is decided to be a draw.
     */
    public void calculateResult() {
        createSectorArrayList();
        int totalEliminated = 0;
        Sector winningSector = null;

        for (Sector s : sectors) {
            if (s.isEliminated()) totalEliminated++;
            if (s.isWinnable() && winningSector != null) return;
            if (s.isWinnable()) winningSector = s;
        }

        if (totalEliminated == sectors.size()) {
            result = EMPTY;
            return;
        }
        if (winningSector != null) {
            result = winningSector.getWinner();
            totalBlack = winningSector.getBlack();
            totalWhite = winningSector.getWhite();
        }
    }

    /**
     * An interface to the Pieces array which returns true if a fence exists immediately in the direction from x,y
     *
     * @param x         x position
     * @param y         y position
     * @param direction direction of fence
     * @return true if fence exists
     */
    private boolean getFence(int x, int y, Direction direction) {
        if (x == boardSize || y == boardSize) return true;
        switch (direction) {
            case UP:
                return boardPieces[x][y].getHFence();
            case LEFT:
                return boardPieces[x][y].getVFence();
            case DOWN:
                return boardPieces[x + 1][y].getHFence();
            case RIGHT:
                return boardPieces[x][y + 1].getVFence();
        }
        return false;
    }

    /**
     * From a given x,y position on the board, this algorithm will recursively invoke itself until every reachable
     * position in the sector (from the initial x,y point) is traversed. Traversed positions are marked as true in
     * visited and the piece is added to the ArrayList p.
     *
     * @param p ArrayList of pieces to return
     * @param x x starting position
     * @param y y starting position
     */
    private void searchAlgorithm(ArrayList<BoardPiece> p, int x, int y) {
        visited[x][y] = true;
        p.add(boardPieces[x][y]);
        if (x > 0) {
            if (!getFence(x, y, Direction.UP) && !visited[x - 1][y])
                searchAlgorithm(p, x - 1, y);
        }
        if (y < boardSize - 1) {
            if (!getFence(x, y, Direction.RIGHT) && !visited[x][y + 1])
                searchAlgorithm(p, x, y + 1);
        }
        if (x < boardSize - 1) {
            if (!getFence(x, y, Direction.DOWN) && !visited[x + 1][y])
                searchAlgorithm(p, x + 1, y);
        }
        if (y > 0) {
            if (!getFence(x, y, Direction.LEFT) && !visited[x][y - 1])
                searchAlgorithm(p, x, y - 1);
        }
    }

    /**
     * Populates the sectors ArrayList with BoardPiece objects by invoking the searchAlgorithm() method. Every board piece
     * is iterated over, and the searchAlgorithm is invoked iff the current board position has not already been visited.
     */
    void createSectorArrayList() {
        for (int i = 0; i < boardSize; i++) {
            for (int j = 0; j < boardSize; j++) {
                visited[i][j] = false;
            }
        }
        sectors.clear();
        for (int i = 0; i < boardSize; i++) {
            for (int j = 0; j < boardSize; j++) {
                if (!visited[i][j]) {
                    ArrayList<BoardPiece> p = new ArrayList<BoardPiece>();
                    searchAlgorithm(p, i, j);
                    Sector s = new Sector(p);
                    sectors.add(s);
                }
            }
        }
    }

    /**
     * Returns the number of black pieces in the winning sector of the board.
     *
     * @return number of black pieces
     */
    public int getTotalBlack() {
        return totalBlack;
    }

    /**
     * Returns the number of the white pieces in the winning sector of the board.
     *
     * @return number of white pieces
     */
    public int getTotalWhite() {
        return totalWhite;
    }

    /**
     * Return the size of the board
     *
     * @return size
     */
    int size() {
        return boardSize;
    }

    /**
     * Returns a string representation of the current board
     *
     * @return string of board contents
     */
    public String getBoard() {
        String s = "";
        for (int i = 0; i < boardSize; i++) {
            for (int j = 0; j < boardSize; j++) {
                s += (boardPieces[i][j].getHFence() ? "=" : "-") + " ";
            }
            s += "\n";
            for (int j = 0; j < boardSize; j++) {
                s += (boardPieces[i][j].getVFence() ? "#" : "|") +
                        (boardPieces[i][j].getType() == BLACK ? "B" : boardPieces[i][j].getType() == WHITE ? "W" : "*");
            }
            s += "\n";
        }
        s += "Sectors: " + sectors.size() + "\n";
        return s;
    }

    /**
     * Returns true if no more pieces can be placed on the board. Positions may still be empty, but these will be in
     * eliminated sectors
     *
     * @return true if only fences can be placed
     */
    public boolean isFull() {
        for (Sector s : sectors) {
            Iterator<BoardPiece> it = s.iterator();
            if (!s.isEliminated()) {
                while (it.hasNext()) {
                    if (it.next().getType() == EMPTY) return false;
                }
            }
        }
        return true;
    }


    /**
     * Return a list of positions that a piece can be placed in
     *
     * @return list of positions
     */
    public ArrayList<Pair> getEmptyPositions() {
        ArrayList<Pair> pairs = new ArrayList<Pair>();
        for (Sector s : sectors) {
            if (!s.isEliminated()) {
                Iterator<BoardPiece> it = s.iterator();
                while (it.hasNext()) {
                    BoardPiece p = it.next();
                    if (p.getType() == EMPTY) pairs.add(new Pair(p.getX(), p.getY()));
                }
            }
        }
        return pairs;
    }

    /**
     * Return a list of positions that a horizontal fence can be placed in
     *
     * @return list of positions
     */
    public ArrayList<Pair> getEmptyHFences() {
        ArrayList<Pair> pairs = new ArrayList<Pair>();
        for (Sector s : sectors) {
            if (!s.isEliminated()) {
                Iterator<BoardPiece> it = s.iterator();
                while (it.hasNext()) {
                    BoardPiece p = it.next();
                    if (!p.getHFence()) pairs.add(new Pair(p.getX(), p.getY()));
                }
            }
        }
        return pairs;

    }


    /**
     * Return a list of positions that a vertical fence can be placed in
     *
     * @return list of positions
     */
    public ArrayList<Pair> getEmptyVFences() {
        ArrayList<Pair> pairs = new ArrayList<Pair>();

        for (Sector s : sectors) {
            if (!s.isEliminated()) {
                Iterator<BoardPiece> it = s.iterator();
                while (it.hasNext()) {
                    BoardPiece p = it.next();
                    if (!p.getVFence()) {
                        pairs.add(new Pair(p.getX(), p.getY()));


                    }
                }
            }
        }
        return pairs;
    }

    /**
     * Method allows piece placement by player objects
     *
     * @param m move object
     * @throws FencingException throw exception if move is illegal
     */
    public void placePiece(Move m) throws FencingException {
        if (!m.HasPiece && !isFull())
            throw new FencingException("Illegal move: More pieces can be placed on board");
        if (m.HasPiece) setPiece(m.PieceRow, m.PieceCol, m.P);
        if (m.HasBarrier) {
            if (m.Direction == 1) setVFence(m.BarrierRow, m.BarrierCol);
            if (m.Direction == 2) setHFence(m.BarrierRow, m.BarrierCol);
        }
    }

}
